Skip to content

gh-81954: Always close ZipFile in __del__ even if the warning fails - #159419

Open
brittanyrey wants to merge 3 commits into
python:mainfrom
brittanyrey:b-zipfile-del-close
Open

brittanyrey wants to merge 3 commits into
python:mainfrom
brittanyrey:b-zipfile-del-close

Conversation

@brittanyrey

@brittanyrey brittanyrey commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

BUG: Now that ZipFile delivers ResourceWarnings during __del__ calls, if there was an issue lazily loading warnings during finalization, the close() call was missed entirely. This left the file in an unexpected state and introduced a possibility of data loss that didn't exist previously.

FIX: Call close() in a finally block.

ZipFile.__del__ emitted its ResourceWarning before calling close().
When warnings.warn() raised, close() was skipped and the central
directory of a writable archive was never written, leaving a corrupt
file. This happens at interpreter shutdown, where resolving the lazy
"warnings" import raises ImportError, and with -W
error::ResourceWarning at any time.

Call close() in a finally block.

@msullivan msullivan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems good to me

Comment thread Lib/zipfile/__init__.py
@brittanyrey

Copy link
Copy Markdown
Contributor Author

Thanks for the review Sully @msullivan !

buf = io.BytesIO()
zf = zipfile.ZipFile(buf, "w")
zf.writestr("f.txt", b"data")
with warnings.catch_warnings(), catch_unraisable_exception() as cm:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be simplified a bit to:

with (warnings.catch_warnings(action="error", category=ResourceWarning),
      catch_unraisable_exception() as cm):
    del zf
    gc_collect()
    self.assertIs(cm.unraisable.exc_type, ResourceWarning)

Another option is making it so we just test with the test_unclosed_at_shutdown style passing -W to the interpreter which would reduce code a bit more while covering the same case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be more cases in the existing test_garbage_collection rather than a new test section. It's adding a new case we want to check

@requires_subprocess()
def test_unclosed_at_shutdown(self):
self.addCleanup(unlink, TESTFN)
script_helper.assert_python_ok("-c", f"""if 1:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still ends up raising an ImportError for zipfile which gets printed to stderr. Is there a way we could make the fix so in this unclosed at interpreter shutdown case we import warnings before interpreter shutdown?

@@ -0,0 +1,3 @@
Fix data loss when a writable :class:`zipfile.ZipFile` that was never closed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user didn't close the file either, this doesn't make a guarantee the zipfile is closed cleanly (GC order is not guaranteed). Definitely improves an edge case when warnings is not importable because the interpreter is shutting down. It would be nice if we could make that warnings reification happen before shutdown. Maybe using _warnings directly in this case? (that is always loaded I think)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinking a bit more here: I think we should just make the warnings import non-lazy: Being lazy with usage only in __del__ leads to this edge case, and the small import time increase is not a bad tradeoff for "we emit warnings properly"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do!


@requires_subprocess()
def test_unclosed_at_shutdown(self):
self.addCleanup(unlink, TESTFN)

@cmaloney cmaloney Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this particular test suite already cleans up TESTFN so I don't think this needs to be done here:

def tearDown(self):
unlink(TESTFN)
unlink(TESTFN2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants